// ===========================================================================
// CONTENT  : CLASS FileUtil
// AUTHOR   : Manfred Duchrow
// VERSION  : 1.1 - 14/03/2003
// HISTORY  :
//  17/05/2002  duma  CREATED
//	14/03/2003	duma	added		->	standardize(), javaFilename()
//
// Copyright (c) 2002-2003, by Manfred Duchrow. All rights reserved.
// ===========================================================================
package org.pf.file;

// ===========================================================================
// IMPORTS
// ===========================================================================
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;

import org.pf.text.StringUtil;

/**
 * This class provides helper methods for file and stream handling. It's an add-on to the java.io package. The service is implemented as a singleton,
 * so use the <b>FileUtil.current()</b> method to get the sole instance.
 * 
 * @author Manfred Duchrow
 * @version 1.1
 */
public class FileUtil {
    // =========================================================================
    // CONSTANTS
    // =========================================================================
    /** The lines.separator from the system properties as a constant */
    public static final String LINE_SEPARATOR = System.getProperty("line.separator");
    protected static final int DEFAULT_BUFFER_SIZE = 1024;

    // =========================================================================
    // CLASS VARIABLES
    // =========================================================================
    private static FileUtil current = new FileUtil();

    // =========================================================================
    // CLASS METHODS
    // =========================================================================

    // =========================================================================
    // CONSTRUCTORS
    // =========================================================================
    /**
     * Initialize the new instance with default values.
     */
    private FileUtil() {
        super();
    } // FileUtil()

    // =========================================================================
    // PUBLIC CLASS METHODS
    // =========================================================================
    public static FileUtil current() {
        return current;
    } // current()

    // =========================================================================
    // PUBLIC INSTANCE METHODS
    // =========================================================================

    /**
     * Copies all data from the iniput stream to the output stream using a buffer with the default size (1024 bytes). After all data is copied both
     * streams will be closed !
     */
    public void copyStream(InputStream inStream, OutputStream outStream) throws IOException {
        this.copyStream(inStream, outStream, DEFAULT_BUFFER_SIZE);
    } // copyStream()

    // -------------------------------------------------------------------------

    /**
     * Copies all data from the iniput stream to the output stream using a buffer of the given size in bytes. After all data is copied both streams
     * will be closed !
     */
    public void copyStream(InputStream inStream, OutputStream outStream, int bufSize) throws IOException {
        byte[] buffer = new byte[bufSize];
        int count;

        try {
            count = inStream.read(buffer);
            while (count > -1) {
                outStream.write(buffer, 0, count);
                count = inStream.read(buffer);
            }
        } finally {
            this.close(inStream);
            this.close(outStream);
        }
    } // copyStream()

    // -------------------------------------------------------------------------

    /**
     * Reads the whole content of the given input stream and returns it as a string. The stream will be closed after calling this method. Even if an
     * exception occured!
     * 
     * @param inStream
     *            The input stream to read
     * @return The text content of the given stream
     */
    public String readTextFrom(InputStream inStream) throws IOException {
        StringWriter writer;

        writer = new StringWriter(1024);
        this.copyText(inStream, writer);
        return writer.toString();
    } // readTextFrom()

    // -------------------------------------------------------------------------

    /**
     * Reads the whole content of the file with the given name and returns it as a string.
     * 
     * @param filename
     *            The name of the text containing file
     */
    public String readTextFrom(String filename) throws IOException {
        FileInputStream inStream;

        inStream = new FileInputStream(filename);
        return this.readTextFrom(inStream);
    } // readTextFrom()

    // -------------------------------------------------------------------------

    /**
     * Reads the whole content of the specified file and returns it as a string.
     */
    public String readTextFrom(File file) throws IOException {
        FileInputStream inStream;

        inStream = new FileInputStream(file);
        return this.readTextFrom(inStream);
    } // readTextFrom()

    // -------------------------------------------------------------------------

    /**
     * Copies all text lines from the specified reader to the given writer. After that the reader will be closed. Even if an exception occurs.
     * 
     * @param reader
     *            The reader which provides the text to copy
     * @param writer
     *            The writer to which the text will be copied
     */
    public void copyText(Reader reader, final StringWriter writer) throws IOException {
        BufferedReader bufReader;
        String line;
        LineProcessor processor;

        bufReader = new BufferedReader(reader);
        try {
            processor = new LineProcessor() {
                public boolean processLine(String line, int lineNo) {
                    if (lineNo > 1)
                        writer.write(LINE_SEPARATOR);

                    writer.write(line);
                    return true;
                }
            };
            this.processTextLines(bufReader, processor);
        } finally {
            bufReader.close();
        }
    } // copyText()

    // ------------------------------------------------------------------------

    /**
     * Reads all text lines from the file with the specified name and passes them one by one to the given line processor. The processing will be
     * terminated, if the end of the text is reached or if the processor returns <b>false</b>.<br>
     * 
     * @param filename
     *            The name of the text file to read
     * @param processor
     *            The processor that receives the lines from the text
     */
    public void processTextLines(String filename, LineProcessor processor) throws IOException {
        FileInputStream inStream;

        if (filename == null)
            throw new IllegalArgumentException("filename must not be null");

        inStream = new FileInputStream(filename);
        try {
            this.processTextLines(inStream, processor);
        } finally {
            inStream.close();
        }
    } // processTextLines()

    // -------------------------------------------------------------------------

    /**
     * Reads all text lines from the specified input stream and passes them one by one to the given line processor. The processing will be terminated,
     * if the end of the text is reached or if the processor returns <b>false</b>.<br>
     * The given input stream will be closed after the execution of this method. Even if an exception occured.
     * 
     * @param inStream
     *            The input stream that contains the text
     * @param processor
     *            The processor that receives the lines from the text
     */
    public void processTextLines(InputStream inStream, LineProcessor processor) throws IOException {
        InputStreamReader reader;

        if (inStream == null)
            throw new IllegalArgumentException("inStream must not be null");

        reader = new InputStreamReader(inStream);
        this.processTextLines(reader, processor);
    } // processTextLines()

    // -------------------------------------------------------------------------

    /**
     * Reads all text lines from the specified reader and passes them one by one to the given line processor. The processing will be terminated, if
     * the end of the text is reached or if the processor returns <b>false</b>.
     * 
     * @param reader
     *            The reader that contains a text stream
     * @param processor
     *            The processor that receives the lines from the text
     */
    public void processTextLines(Reader reader, LineProcessor processor) throws IOException {
        BufferedReader bufReader;
        String line;
        int counter = 0;
        boolean continue_reading = true;

        if (reader == null)
            throw new IllegalArgumentException("reader must not be null");

        if (processor == null)
            throw new IllegalArgumentException("processor must not be null");

        bufReader = new BufferedReader(reader);
        while (continue_reading && bufReader.ready()) {
            line = bufReader.readLine();
            if (line == null)
                break;

            counter++;
            continue_reading = processor.processLine(line, counter);
        }
    } // processTextLines()

    // ------------------------------------------------------------------------

    /**
     * Close the given stream ignoring any exception. Returns true, if the stream was closed successfully, false otherwise
     */
    public boolean close(InputStream stream) {
        if (stream == null) {
            return false;
        }
        try {
            stream.close();
            return true;
        } catch (IOException e) {
            return false;
        }
    } // close()

    // -------------------------------------------------------------------------

    /**
     * Close the given stream ignoring any exception. Returns true, if the stream was closed successfully, false otherwise
     */
    public boolean close(OutputStream stream) {
        if (stream == null) {
            return false;
        }
        try {
            stream.close();
            return true;
        } catch (IOException e) {
            return false;
        }
    } // close()

    // -------------------------------------------------------------------------

    /**
     * Convert the filename to a canonical (see java.io.File.getCanonicalPath()) format and replace any backslashes '\' by slashes ('/'). If possible
     * all "." and ".." elements in the path are eliminated.
     * 
     * @param filename
     *            The filename which has to be standardized
     * @return An absolute filename that uses slashes to separate its elements
     */
    public String standardize(String filename) {
        if (filename == null)
            return null;

        return this.standardizeFilename(filename);
    } // standardize()

    // -------------------------------------------------------------------------

    /**
     * Returns the given filename in the platform independent way that Java understands. That is all elements are separated by a forward slash rather
     * than back slashes as on Windows systems.
     * 
     * @param filename
     *            The name to be modified
     */
    public String javaFilename(String filename) {
        if (filename == null)
            return null;

        return filename.replace('\\', '/');
    } // javaFilename()

    // -------------------------------------------------------------------------

    // =========================================================================
    // PROTECTED INSTANCE METHODS
    // =========================================================================
    protected void copyText(InputStream inStream, StringWriter writer) throws IOException {
        this.copyText(new InputStreamReader(inStream), writer);
    } // copyText()

    // ------------------------------------------------------------------------

    protected String standardizeFilename(String filename) {
        String[] nameElements;
        boolean hasDriveLetter;
        boolean startedFromRoot;
        boolean isAbsolute;
        int index;

        filename = this.javaFilename(filename);
        startedFromRoot = filename.startsWith("/");
        nameElements = this.str().parts(filename, "/");
        if (nameElements.length > 0) {
            hasDriveLetter = nameElements[0].endsWith(":");
            if (hasDriveLetter) {
                nameElements[0] = nameElements[0].toUpperCase();
            } else {
                if (startedFromRoot) {
                    nameElements = this.str().append(new String[] { "" }, nameElements);
                }
            }
            isAbsolute = hasDriveLetter || startedFromRoot;
            for (int i = 0; i < nameElements.length; i++) {
                if (".".equals(nameElements[i])) {
                    nameElements[i] = null;
                } else {
                    if ("..".equals(nameElements[i])) {
                        index = this.indexOfPreceedingNotNullElement(nameElements, i - 1);
                        if (index >= 0) {
                            if ((index > 0) || (!isAbsolute)) {
                                nameElements[i] = null;
                                nameElements[index] = null;
                            }
                        }
                    }
                }
            }
            nameElements = this.str().removeNull(nameElements);
            return this.str().asString(nameElements, "/");
        } else {
            return "";
        }
    } // standardizeFilename()

    // -------------------------------------------------------------------------

    protected int indexOfPreceedingNotNullElement(String[] elements, int start) {
        for (int i = start; i >= 0; i--) {
            if (elements[i] != null) {
                if ("..".equals(elements[i])) // This is not a valid not null element
                {
                    return -1;
                } else {
                    return i;
                }
            }
        }
        return -1;
    } // indexOfPreceedingNotNullElement()

    // -------------------------------------------------------------------------

    protected StringUtil str() {
        return StringUtil.current();
    } // str()

    // -------------------------------------------------------------------------

} // class FileUtil